namespace NTrace
{
  using System;
  using System.Collections.Generic;
  using System.IO;
  using System.Security.Cryptography;
  using System.Text;
  using System.Text.RegularExpressions;

  public class TraceMessageFile
  {
    private static Regex formatSpecifiers = new Regex(
      "(?<!{){(?<argumentIndex>\\d+)}",
      RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);

    private PreprocessorContext context;
    private String mungedSourceFileName;
    private Guid messageGuid;
    private int nextMessageNumber = 10;
    private StringBuilder buffer;

    internal TraceMessageFile(PreprocessorContext context)
    {
      System.Diagnostics.Debug.Assert(context != null, "Expected a non-null trace context.");
      if (context == null) throw new ArgumentNullException("context");

      this.context = context;
      this.mungedSourceFileName = System.IO.Path.GetFileName(this.context.CodeFile).ToLower().Replace('.', '_');
      this.messageGuid = this.CalculateMessageGuid();
      this.buffer = new StringBuilder();

      this.WriteMessageControlFile();
      this.AddHeader();
      String traceMessageFileName = System.IO.Path.ChangeExtension(
        this.messageGuid.ToString("B"),
        ".tmf");
    }

    internal Guid MessageGuid
    {
      get { return messageGuid; }
    }

    private Guid CalculateMessageGuid()
    {
      using (MD5 sha1 = MD5.Create())
      {
        using (System.IO.Stream fileContents = new FileStream(this.context.CodeFile, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
          byte[] hash = sha1.ComputeHash(fileContents);
          return new Guid(hash);
        }
      }
    }

    private void WriteMessageControlFile()
    {
      String fileName = String.Format("{0:D}.tmc", this.context.Environment.ProviderId);
      System.Collections.Generic.SortedDictionary<int, String> sortedFlags = new SortedDictionary<int, String>();
      foreach (String flagName in this.context.Environment.Configuration.TraceFlags.Keys)
      {
        sortedFlags[this.context.Environment.Configuration.TraceFlags[flagName]] = flagName;
      }

      using (System.IO.StreamWriter writer = new System.IO.StreamWriter(
        System.IO.Path.Combine(
          this.context.Environment.OutputFolder,
          fileName)))
      {
        writer.WriteLine("//PDB:  ");
        
        writer.WriteLine(
          "//PDB:  Last Updated: {0:U} UTC by {1}",
        DateTime.Now,
        System.IO.Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase));

        String upperCaseID = String.Format("{0:D}", this.context.Environment.ProviderId).ToUpper();
        
        writer.WriteLine(upperCaseID);

        writer.WriteLine(this.context.Environment.FriendlyName);

        foreach (int flagValue in sortedFlags.Keys)
        {
          writer.WriteLine(sortedFlags[flagValue]);
        }
      }
    }

    private void AddHeader()
    {
      this.buffer.AppendLine("//PDB:  ");
      
      this.buffer.AppendFormat(
        "//PDB:  Last Updated: {0:U} UTC by {1}", 
        DateTime.Now,
        System.IO.Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase));

      this.buffer.AppendLine();

      this.buffer.AppendFormat(
        "{0:D} {1} // SRC={2} MJ= MN=",
        this.messageGuid,
        this.context.Environment.FriendlyName,
        System.IO.Path.GetFileName(this.context.CodeFile));

      this.buffer.AppendLine();
    }

    internal int AddMessage(
      int line, 
      String message, 
      int flag, 
      int level, 
      IList<String> arguments, 
      String fullMethodName,
      bool staticMethod)
    {
      const Int32 BaseOffset = 10; // user-defined messages start at 10
      Int32 argumentOffset = BaseOffset;
      //#typev  <<file>><<line>> <<messagenumber>> "%0 [<<string>>[%<<10-based parameter number>>!s!]...]" //   LEVEL=TL_INFO FLAGS=TF_DIAG FUNC=<<Fully-qualified method name>>
      //{
      //[arg[0...], ItemPWString -- <<parameter number>>]...
      //}

      // This is a new message, so increment the current message number
      int messageNumber = this.nextMessageNumber++;

      // Count the number of format string arguments
      // Walk the list of format specifiers
      StringBuilder formatString = new StringBuilder();
      int lastIndex = 0;

      if (!staticMethod)
      {
        // Add the hash code placeholder
        formatString.Append("(%10!p!) ");
        argumentOffset++;
      }

      Match specifier = formatSpecifiers.Match(message, lastIndex);
      while (specifier != null && specifier.Success )
      {
        formatString.Append(message, lastIndex, specifier.Index - lastIndex);

        Int32 argumentIndex = Int32.Parse(specifier.Groups["argumentIndex"].Value);
        
        if (argumentIndex > arguments.Count - 1) throw new PreprocessorException(
          508,
          "Format string argument index is greater than the number of supplied arguments.");

        argumentIndex += argumentOffset;
        formatString.AppendFormat("%{0}!s!", argumentIndex);

        lastIndex = specifier.Index + specifier.Length;
        specifier = specifier.NextMatch();
      }
      formatString.Append(message.Substring(lastIndex));

      this.buffer.AppendFormat(
        "#typev {0}{1} {2} \"%0",
        this.mungedSourceFileName,
        line,
        messageNumber);

      this.buffer.Append(formatString);
      this.buffer.AppendFormat(
        "\" //   LEVEL={0} FLAGS={1} FUNC={2}",
        level, // TODO: Get TL_XX string
        flag, // TODO: Get flag string
        fullMethodName);

      this.buffer.AppendLine();

      this.buffer.AppendLine("{");

      if (!staticMethod)
      {
        // BUGBUG: We are currently passing GetHashCode as a string argument
        //this.buffer.AppendLine("    this.GetHashCode(), ItemPtr -- 10");
        this.buffer.AppendLine("    this.GetHashCode(), ItemPWString -- 10");
      }

      for (int index = 0; index < arguments.Count; index++)
      {
        this.buffer.AppendFormat("    {0}, ItemPWString -- {1}\n", arguments[index], index + argumentOffset);
        this.buffer.AppendLine();
      }

      this.buffer.AppendLine("}");

      return messageNumber;
    }

    public void Write()
    {
      String fileName = String.Format("{0:D}.tmf", this.messageGuid);
      using (System.IO.StreamWriter writer = new System.IO.StreamWriter(
        System.IO.Path.Combine(
          this.context.Environment.OutputFolder,
          fileName)))
      {
        writer.Write(this.buffer.ToString());
      }
    }
  }
}
